/*
 * Copyright (c) 2023 fred
 * Licensed under the Apache License,Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
import HashMap from '@ohos.util.HashMap';
import { Normalizer } from '../internal/Normalizer';
import { ParseSettings } from './ParseSettings';
import { Validate } from '../helper/Validate';
import hilog from '@ohos.hilog';
/**
 * HTML Tag capabilities.
 *
 */
export class Tag {
  // map of known tags
  private static readonly _tags = new HashMap<String, Tag>();

  private _tagName:string
  // always the lower case version of this tag, regardless of case preservation mode
  private _normalName:string
  // block
  private _isBlock:boolean = true
  // should be formatted as a block
  private _formatAsBlock:boolean = true
  // can hold nothing; e.g. img
  private _empty:boolean = false
  // can self close (<foo />). used for unknown tags that self close, without forcing them as empty.
  private _selfClosing:boolean = false
  // for pre, textarea, script etc
  private _preserveWhitespace:boolean = false
  // a control that appears in forms: input, textarea, output etc
  private _formList:boolean = false
  // a control that can be submitted in a form: input etc
  private _formSubmit:boolean = false

  private constructor(tagName:string) {
    this._tagName = tagName
    this._normalName = Normalizer.lowerCase(tagName)
  }

  /**
   * Get this tag's name.
   *
   * @return the tag's name
   */
  public getName():string {
    return this._tagName
  }

  /**
   * Get this tag's normalized (lowercased) name.
   * @return the tag's normal name.
   */
  public normalName():string {
    return this._normalName
  }

  /**
   * Get a Tag by name. If not previously defined (unknown), returns a new generic tag, that can do anything.
   * <p>
   * Pre-defined tags (P, DIV etc) will be ==, but unknown tags are not registered and will only .equals().
   * </p>
   *
   * @param tagName Name of tag, e.g. "p". Case insensitive.
   * @param settings used to control tag name sensitivity
   * @return The tag, either defined or new generic.
   */
  public static valueOf(tagName:string,settings:ParseSettings = ParseSettings.preserveCase):Tag {
    Validate.notNull(tagName)

    // hilog.info(0x0000, 'testTag', '%{public}s', `valueOf tags:${Tag._tags.length}`);
    let tag = Tag._tags.get(tagName)
    if (tag === undefined || tag === null) {
      // the name we'll use
      tagName = settings.normalizeTag(tagName)
      Validate.notEmpty(tagName,'Tag valueOf tagName must not be empty')
      // the lower-case name to get tag settings off
      let normalName = Normalizer.lowerCase(tagName);
      tag = Tag._tags.get(normalName)
      if (tag === undefined || tag === null) {
        // not defined: create default; go anywhere, do anything! (incl be inside a <p>)
        tag = new Tag(tagName)
        tag._isBlock = false
      } else if (settings.preserveTagCase() && tagName !== normalName) {
        // get a new version vs the static one, so name update doesn't reset all
        tag = tag.clone()
        tag._tagName = tagName
      }
    }
    return tag
  }

  /**
   * Gets if this is a block tag.
   *
   * @return if block tag
   */
  public isBlock():boolean {
    return this._isBlock;
  }

  /**
   * Gets if this tag should be formatted as a block (or as inline)
   *
   * @return if should be formatted as block or inline
   */
  public formatAsBlock():boolean {
    return this._formatAsBlock;
  }

  /**
   * Gets if this tag is an inline tag.
   *
   * @return if this tag is an inline tag.
   */
  public isInline():boolean {
    return !this._isBlock;
  }

  /**
   * Get if this is an empty tag
   *
   * @return if this is an empty tag
   */
  public isEmpty():boolean {
    return this._empty;
  }

  /**
   * Get if this tag is self closing.
   *
   * @return if this tag should be output as self closing.
   */
  public  isSelfClosing():boolean {
    return this._empty || this._selfClosing;
  }

  /**
   * Get if this is a pre-defined tag, or was auto created on parsing.
   *
   * @return if a known tag
   */
  public isKnownTag():boolean {
    return Tag._tags.hasKey(this._tagName);
  }

  public static isKnownTag(tagName:string):boolean {
  return Tag._tags.hasKey(tagName);
}

  /**
   * Get if this tag should preserve whitespace within child text nodes.
   *
   * @return if preserve whitespace
   */
  public preserveWhitespace():boolean {
    return this._preserveWhitespace;
  }

  /**
   * Get if this tag represents a control associated with a form. E.g. input, textarea, output
   * @return if associated with a form
   */
  public isFormListed():boolean {
    return this._formList;
  }

  /**
   * Get if this tag represents an element that should be submitted with a form. E.g. input, option
   * @return if submittable with a form
   */
  public isFormSubmittable():boolean {
    return this._formSubmit;
  }

  setSelfClosing():Tag{
    this._selfClosing = true
    return this
  }
  public equals(o:any) :boolean {
    if (this === o) {
      return true
    }
    if (!(o instanceof Tag)) {
      return false
    }
    let tag = o as Tag
    if (this._tagName !== tag._tagName) {
      return false
    }
    if (this._empty !== tag._empty) {
      return false
    }
    if (this._formatAsBlock !== tag._formatAsBlock) {
      return false
    }
    if (this._isBlock !== tag._isBlock) {
      return false
    }
    if (this._preserveWhitespace !== tag._preserveWhitespace) {
      return false
    }
    if (this._selfClosing !== tag._selfClosing) {
      return false
    }
    if (this._formList !== tag._formList) {
      return false
    }
    return this._formSubmit === tag._formSubmit
  }

  public hashCode():number {
    let result = this._tagName.charCodeAt(0)
    result = 31* result + (this._isBlock ? 1:0)
    result = 31* result + (this._formatAsBlock ? 1:0)
    result = 31* result + (this._empty ? 1:0)
    result = 31* result + (this._selfClosing ? 1:0)
    result = 31* result + (this._preserveWhitespace ? 1:0)
    result = 31* result + (this._formList? 1:0)
    result = 31* result + (this._formSubmit ? 1:0)
    return result
  }

  public toString():string {
    return this._tagName
  }

  protected clone():Tag {
    try {
      let clone = new Tag('')
      return Object.assign(clone, this);
    } catch (e) {
      throw new Error(JSON.parse(e))
    }
  }

  // internal static initialisers:
  // prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources
  // deprecated but still known / special handling
  private static readonly blockTags:string[] = [ "html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame",
    "noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6",
    "ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", "ins",
    "del", "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th",
    "td", "video", "audio", "canvas", "details", "menu", "plaintext", "template", "article", "main",
    "svg", "math", "center", "template",
    "dir", "applet", "marquee", "listing" ]
  // deprecated but still known / special handling
  private static readonly inlineTags:string[] = ["object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd",
    "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "rtc", "a", "img", "br", "wbr", "map", "q",
    "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup",
    "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track",
    "summary", "command", "device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track",
    "data", "bdi", "s", "strike", "nobr",
    "rb"]
  private static readonly emptyTags:string[] = ["meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command",
    "device", "area", "basefont", "bgsound", "menuitem", "param", "source", "track"]
  // todo - rework this to format contents as inline; and update html emitter in Element. Same output, just neater.
  private static readonly formatAsInlineTags:string[] = ["title", "a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style",
    "ins", "del", "s"]
  // script is not here as it is a data node, which always preserve whitespace
  private static readonly preserveWhitespaceTags:string[] =["pre", "plaintext", "title", "textarea"]
  // todo: I think we just need submit tags, and can scrub listed
  private static readonly formListedTags:string[] = ["button", "fieldset", "input", "keygen", "object", "output", "select", "textarea"]
  private static readonly formSubmitTags:string[] = ["input", "keygen", "object", "select", "textarea"]
  private static _initializer = (() => {
    for (let tagName of Tag.blockTags) {
      let tag = new Tag(tagName)
      Tag.register(tag)
    }
    for (let tagName of Tag.inlineTags) {
      let tag = new Tag(tagName)
      tag._isBlock = false
      tag._formatAsBlock = false
      Tag.register(tag)
    }
    // mods:
    for (let tagName of Tag.emptyTags) {
      let tag = Tag._tags.get(tagName)
      Validate.notNull(tag)
      tag._empty = true
    }
    for (let tagName of Tag.formatAsInlineTags) {
      let tag = Tag._tags.get(tagName)
      Validate.notNull(tag)
      tag._formatAsBlock = false
    }
    for (let tagName of Tag.preserveWhitespaceTags) {
      let tag = Tag._tags.get(tagName)
      Validate.notNull(tag)
      tag._preserveWhitespace = true
    }
    for (let tagName of Tag.formListedTags) {
      let tag = Tag._tags.get(tagName)
      Validate.notNull(tag)
      tag._formList = true
    }
    for (let tagName of Tag.formSubmitTags) {
      let tag = Tag._tags.get(tagName)
      Validate.notNull(tag)
      tag._formSubmit = true
    }
  })();
  private static register(tag:Tag) {
    Tag._tags.set(tag._tagName, tag);
  }

}
